Package org.python.pydev.core.path_watch

Source Code of org.python.pydev.core.path_watch.PathWatch$PollThread

/**
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package org.python.pydev.core.path_watch;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import name.pachler.nio.file.ClosedWatchServiceException;
import name.pachler.nio.file.FileSystems;
import name.pachler.nio.file.Path;
import name.pachler.nio.file.Paths;
import name.pachler.nio.file.StandardWatchEventKind;
import name.pachler.nio.file.WatchEvent;
import name.pachler.nio.file.WatchEvent.Kind;
import name.pachler.nio.file.WatchKey;
import name.pachler.nio.file.WatchService;
import name.pachler.nio.file.ext.ExtendedWatchEventKind;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.python.pydev.core.ListenerList;
import org.python.pydev.core.log.Log;

import com.aptana.shared_core.io.FileUtils;
import com.aptana.shared_core.string.FastStringBuffer;

/**
* @author fabioz
*
* Service to watch filesystem changes at a given path. Works with JPathWatch.
*
* Multiple events are stacked and reported from time to time.
*
* When a key that is tracked is removed from the filesystem, it enters in a poll job (invalidPathsRestorer)
* which will notify when it's recreated (note that it's only (re)scheduled if there is some available invalid path).
*/
public class PathWatch {

    /**
     * The service that'll give us notifications.
     */
    private WatchService watchService;

    /**
     * If != null, logs will be added to this buffer.
     */
    public static FastStringBuffer log;

    /**
     * The path being watched and the stacker object that'll stack many requests into one.
     *
     * The stacker object contains the actual key in the watchService (although it may be none if the key
     * becomes invalid).
     */
    private Map<Path, EventsStackerRunnable> pathToStacker = new HashMap<Path, EventsStackerRunnable>();
    private final Object keyToPathLock = new Object();
    private Map<WatchKey, Path> keyToPath = new HashMap<WatchKey, Path>();

    private final Object invalidPathsLock = new Object();
    private volatile Set<EventsStackerRunnable> invalidPaths = new HashSet<EventsStackerRunnable>();

    /*default*/Set<EventsStackerRunnable> getInvalidPaths() {
        synchronized (invalidPathsLock) {
            //Always return a copy!
            return new HashSet<EventsStackerRunnable>(invalidPaths);
        }
    }

    private final PollThread pollThread;
    private final Object lock = new Object();

    private volatile List<Runnable> runnables = new ArrayList<Runnable>();
    private final Job jobRunRunnables = new Job("PathWatch notifier") {

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            //Clients will actually be notified in this job.
            List<Runnable> curr = runnables;
            runnables = new ArrayList<Runnable>();
            for (Runnable runnable : curr) {
                try {
                    runnable.run();
                } catch (Exception e) {
                    Log.log(e);
                }
            }
            return Status.OK_STATUS;
        }
    };

    public static int RECHECK_INVALID_PATHS_EACH = 4000;
    private final Job invalidPathsRestorer = new Job("Invalid paths restorer") {

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            synchronized (invalidPathsLock) {
                if (log != null) {
                    log.append('.');
                }

                Set<EventsStackerRunnable> remove = new HashSet<EventsStackerRunnable>();
                for (Iterator<EventsStackerRunnable> it = invalidPaths.iterator(); it.hasNext();) {
                    EventsStackerRunnable r = it.next();
                    IFilesystemChangesListener[] listeners = r.list.getListeners();
                    if (listeners.length == 0) {
                        if (log != null) {
                            log.append("Removing stacker from invalid list (because it has no listeners): ")
                                    .appendObject(r).append('\n');
                        }
                        remove.add(r); //remove last iterated (no longer watched)
                    } else {
                        File f = new File(r.watchedPath.toString());
                        if (f.exists()) {
                            for (IFilesystemChangesListener listener : listeners) {
                                listener.added(f);
                            }

                            try {
                                WatchKey key = r.watchedPath.register(watchService,
                                        StandardWatchEventKind.ENTRY_CREATE, StandardWatchEventKind.ENTRY_DELETE,
                                        StandardWatchEventKind.ENTRY_MODIFY, StandardWatchEventKind.OVERFLOW,
                                        ExtendedWatchEventKind.KEY_INVALID);
                                //only add to be removed if it was successful...
                                r.key = key;
                                synchronized (keyToPathLock) {
                                    keyToPath.put(key, r.watchedPath);
                                }

                                if (log != null) {
                                    log.append("Removing stacker from invalid list because it became valid again: ")
                                            .appendObject(r).append('\n');
                                }

                                remove.add(r); //remove last iterated (valid again)
                            } catch (UnsupportedOperationException uox) {
                                Log.log(uox);

                            } catch (IOException iox) {
                                //Ignore: it may not exist now, but may start existing later on...
                                if (log != null) {
                                    log.append("IOException when trying to make valid: " + r.watchedPath);
                                }

                            } catch (Throwable e) {
                                Log.log(e);
                            }

                        }
                    }
                }

                invalidPaths.removeAll(remove);
                //Re-add the ones not removed...
                int size = invalidPaths.size();
                if (log != null) {
                    if (size < 0) {
                        //This could happen when access to invalidPaths is not properly synched with invalidPathsLock!
                        log.append("\nBUG BUG BUG: Size: ").append(size).append('\n');
                    }
                }
                if (size > 0) {
                    this.schedule(RECHECK_INVALID_PATHS_EACH);
                    if (log != null) {
                        log.append("!");
                    }
                } else {
                    if (log != null) {
                        log.append("NOT rescheduling; size=").append(size).append(";invalidPaths=")
                                .appendObject(invalidPaths).append('\n');
                    }
                }
            }
            return Status.OK_STATUS;
        }
    };

    /**
     * After receiving a change, it'll only be notified after this time elapses (in millis).
     * This means that while we have a change it may be that what's reported actually changes
     * (i.e.: if a file is added and removed, only the removal will be recorded).
     */
    public static int TIME_BEFORE_NOTIFY = 250;

    private PathWatch() {
        watchService = FileSystems.getDefault().newWatchService();
        pollThread = new PollThread();
        pollThread.start();
    }

    private class PollThread extends Thread {

        public void run() {

            for (;;) {
                // take() will block until a file has been created/deleted
                WatchKey signalledKey;
                try {
                    signalledKey = watchService.take();
                } catch (InterruptedException ix) {
                    // we'll ignore being interrupted
                    if (log != null) {
                        log.append("Interrupted\n");
                    }
                    continue;
                } catch (ClosedWatchServiceException cwse) {
                    // other thread closed watch service
                    System.out.println("watch service closed, terminating.");
                    break;
                }

                List<WatchEvent<?>> list;
                Path watchedPath;
                EventsStackerRunnable stacker;

                synchronized (lock) {
                    synchronized (keyToPathLock) {
                        watchedPath = keyToPath.get(signalledKey);
                    }
                    if (watchedPath == null) {
                        continue;
                    }

                    // get list of events from key
                    list = signalledKey.pollEvents();

                    stacker = pathToStacker.get(watchedPath);
                    if (stacker == null) {
                        //if the stacker does not exist, go on without rescheduling the key!
                        if (log != null) {
                            log.append("Stacker for: ").appendObject(watchedPath).append("is null\n");
                        }
                        continue;
                    }

                    runnables.add(stacker);

                    for (WatchEvent<?> e : list) {
                        Path context = (Path) e.context();
                        Path resolve = watchedPath.resolve(context);
                        File file = new File(resolve.toString());
                        Kind<?> kind = e.kind();
                        if (log != null) {
                            log.append("Event: ").appendObject(e).append('\n');
                        }
                        if (kind == StandardWatchEventKind.OVERFLOW) {
                            if (!file.exists()) {
                                //It may be that it became invalid...
                                synchronized (keyToPathLock) {
                                    keyToPath.remove(signalledKey);
                                }
                                stacker.key = null;
                                addInvalidPath(stacker);
                                stacker.removed(file);
                            } else {
                                // VERY IMPORTANT! call reset() AFTER pollEvents() to allow the
                                // key to be reported again by the watch service.
                                signalledKey.reset();
                                if (log != null) {
                                    log.append("Key reset to hear changes");
                                }
                            }
                            //On an overflow, wait a bit and signal that all files being watched were removed,
                            //do a list and say that the current files were added again.
                            stacker.overflow(file);

                        } else {
                            if (kind == StandardWatchEventKind.ENTRY_CREATE
                                    || kind == StandardWatchEventKind.ENTRY_MODIFY) {
                                // VERY IMPORTANT! call reset() AFTER pollEvents() to allow the
                                // key to be reported again by the watch service.
                                signalledKey.reset();
                                if (log != null) {
                                    log.append("Key reset to hear changes");
                                }

                                stacker.added(file);

                            } else if (kind == StandardWatchEventKind.ENTRY_DELETE) {
                                // VERY IMPORTANT! call reset() AFTER pollEvents() to allow the
                                // key to be reported again by the watch service.
                                signalledKey.reset();
                                if (log != null) {
                                    log.append("Key reset to hear changes");
                                }
                                stacker.removed(file);

                            } else if (kind == ExtendedWatchEventKind.KEY_INVALID) {
                                //Invalidated means it was removed... (so, no need to reschedule to listen again)
                                synchronized (keyToPathLock) {
                                    keyToPath.remove(signalledKey);
                                }
                                stacker.key = null;
                                addInvalidPath(stacker);
                                stacker.removed(file);
                            }
                        }
                    }
                }
                if (runnables.size() > 0) {
                    jobRunRunnables.schedule(TIME_BEFORE_NOTIFY);
                }
            }
        }
    }

    private static PathWatch singleton = null;

    public static PathWatch get() {
        if (singleton == null) {
            singleton = new PathWatch();
        }
        return singleton;
    }

    public void stopTrack(File path, IFilesystemChangesListener listener) {
        Assert.isNotNull(path);
        Assert.isNotNull(listener);

        Path watchedPath = Paths.get(FileUtils.getFileAbsolutePath(path));

        if (log != null) {
            log.append("STOP Track: ").appendObject(path).append("Listener: ").appendObject(listener).append('\n');
        }

        synchronized (lock) {
            EventsStackerRunnable stacker = pathToStacker.get(watchedPath);

            if (stacker != null && stacker.list != null) {
                ListenerList<IFilesystemChangesListener> list = stacker.list;
                list.remove(listener);
                if (list.getListeners().length == 0) {
                    pathToStacker.remove(watchedPath);
                    synchronized (keyToPathLock) {
                        keyToPath.remove(stacker.key);
                    }
                    synchronized (invalidPathsLock) {
                        if (log != null) {
                            log.append("Remove from invalid paths (no listeners): ").appendObject(stacker).append('\n');
                        }
                        invalidPaths.remove(stacker);
                    }
                }
            }
        }
    }

    /**
     * A listener will start tracking changes at the given path.
     */
    public void track(File path, IFilesystemChangesListener listener) {
        Assert.isNotNull(path);
        Assert.isNotNull(listener);

        Path watchedPath = Paths.get(FileUtils.getFileAbsolutePath(path));

        synchronized (lock) {
            EventsStackerRunnable stacker = pathToStacker.get(watchedPath);
            if (stacker != null) {
                //already being tracked -- or already in invalid list ;)
                stacker.list.add(listener);

                return;
            }

            if (log != null) {
                log.append("Track: ").appendObject(path).append("Listener: ").appendObject(listener).append('\n');
            }
            boolean add = true;
            WatchKey key = null;
            try {
                key = watchedPath.register(watchService, StandardWatchEventKind.ENTRY_CREATE,
                        StandardWatchEventKind.ENTRY_DELETE, StandardWatchEventKind.ENTRY_MODIFY,
                        StandardWatchEventKind.OVERFLOW, ExtendedWatchEventKind.KEY_INVALID);
            } catch (UnsupportedOperationException uox) {
                if (log != null) {
                    log.append("UnsupportedOperationException: ").appendObject(uox).append('\n');
                }
                add = false;
                Log.log(uox);

            } catch (IOException iox) {
                //Ignore: it may not exist now, but may start existing later on...

            } catch (Throwable e) {
                if (log != null) {
                    log.append("Throwable: ").appendObject(e).append('\n');
                }
                add = false;
                Log.log(e);
            }

            if (add) {
                if (stacker == null) {
                    stacker = new EventsStackerRunnable(key, watchedPath, new ListenerList<IFilesystemChangesListener>(
                            IFilesystemChangesListener.class));
                    pathToStacker.put(watchedPath, stacker);
                }
                stacker.list.add(listener);

                if (key != null) {
                    synchronized (keyToPathLock) {
                        keyToPath.put(key, watchedPath);
                    }
                } else {
                    //Will go to our poll service to start tracking when it becomes valid...
                    addInvalidPath(stacker);
                }
            }
        }
    }

    private void addInvalidPath(EventsStackerRunnable stacker) {
        if (log != null) {
            log.append("addInvalidPath: ").appendObject(stacker).append('\n');
        }

        synchronized (invalidPathsLock) {
            invalidPaths.add(stacker);
        }
        invalidPathsRestorer.schedule(RECHECK_INVALID_PATHS_EACH);
    }
}
TOP

Related Classes of org.python.pydev.core.path_watch.PathWatch$PollThread

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.